package nl.ViewerServer.SessionManager.Objects;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import nl.ViewerServer.Application;
import nl.ViewerServer.Database.SessionManagerDatabaseConnection;
import nl.ViewerServer.Exceptions.ParameterException;
import nl.ViewerServer.Exceptions.UserRequestException;
import nl.ViewerServer.ServerManager.DataManager.Objects.Modality;
import nl.ViewerServer.ServerManager.DataManager.Objects.Parameter;
import nl.ViewerServer.ServerManager.DataManager.Objects.Visualization;
import nl.ViewerServer.ServerManager.DataManager.Objects.Parameter.ParameterType;
import nl.ViewerServer.ServerManager.DataManager.Objects.Parameter.SetterType;
import nl.ViewerServer.ServerManager.DataManager.PipelineObjects.Pipeline;
import nl.ViewerServer.ServerManager.DataManager.PipelineObjects.PipelineOutput;
import nl.ViewerServer.ServerManager.DataManager.PipelineObjects.PipelineSource;
import nl.ViewerServer.ServerManager.UserManager.Objects.User;
import nl.ViewerServer.SessionManager.SessionManager;

import org.apache.log4j.Logger;

public class Session implements Runnable {
   private static Logger log = Logger.getLogger( Session.class );

   public static enum Status {
      IDLE, QUEUED, WORKING, FAILED, CLOSED
   }

   public final long                                       sessionId;

   public final transient int                              userId;
   public final transient String                           remoteAddress;
   public final transient String                           remoteHostname;
   protected String                                        algorithm                  = "Initializing";
   protected int                                           progress                   = 0;
   protected String                                        reason                     = null;

   protected final transient Visualization                 visualization;
   protected final transient Modality                      modality;

   private final ArrayList<Job>                            waitingJobs                = new ArrayList<Job>();
   private Job                                             currentJob                 = null;

   private final transient HashMap<String, Parameter>      sessionParameters          = new HashMap<String, Parameter>();
   private final transient HashMap<String, Parameter>      alignedModalityParameters  = new HashMap<String, Parameter>();
   private Status                                          status                     = Status.IDLE;

   private final transient PipelineSource                  pipelineSource;
   private final transient PipelineSource[]                alignedModalityPipelineSources;
   private final transient Pipeline                        pipeline;
   private final transient HashMap<String, PipelineOutput> pipelineOutputs;
   private transient PipelineOutput                        selectedOutput             = null;

   // this variable is used for caching purposes (otherwise each call has to collect all parameters, while this
   // list will not change often)
   private transient HashMap<String, Parameter>            allParameters              = null;
   private transient HashMap<String, Parameter>            allVisualizationParameters = null;

   public final ArrayList<Job>                             alteredJobs                = new ArrayList<Job>();

   private transient int                                   retryCount                 = 0;

   protected transient boolean                             sessionAltered             = true;

   private transient long                                  lastAction                 = System.currentTimeMillis();

   // The following readwrite lock is used for the serialization functionality for the getStatus call,
   // the idea is as follows. The ReentrantReadWriteLock will accept read locks as long as there are no
   // (requests) for write locks. As soon as a write lock is requested and after the readlocks are released, the write
   // lock is granted (for the serialization procedure). After the serialization the write lock is removed, allowing
   // other methods to access the session variables.

   private transient ReentrantReadWriteLock                rwl                        = new ReentrantReadWriteLock();

   public final transient ReentrantReadWriteLock.ReadLock  readLock;
   public final transient ReentrantReadWriteLock.WriteLock writeLock;

   public final transient int                              requestedThreads;

   public Session( long sessionId, User u, String remoteAddress, String remoteHostname, Modality modality,
         Visualization visualization, int requestedThreads ) throws SQLException {
      super();

      this.sessionId = sessionId;
      this.userId = u.userId;
      this.remoteAddress = remoteAddress;
      this.remoteHostname = remoteHostname;
      this.modality = modality;
      this.visualization = visualization;
      this.requestedThreads = requestedThreads;

      readLock = rwl.readLock();
      writeLock = rwl.writeLock();

      addDefaultSessionParameters();

      // modality specifies the source
      SessionManagerDatabaseConnection db = SessionManager.getInstance().database;
      pipelineSource = db.createPipelineSource( modality.getSource(), modality.getModalityId(),
            visualization.visualizationId );
      log.info( "PipelineSource (" + pipelineSource.name + ") initialized" );

      if( visualization.requiresAlignedModalities ) {
         // the aligned sources
         ArrayList<Modality> aligned = modality.getAlignmentsTo();
         alignedModalityPipelineSources = new PipelineSource[ aligned.size() ];
         String trueArray = "[True";
         String emptyLabels = "[";
         for( int i = 0; i < aligned.size(); i++ ) {
            if( i > 0 ) {
               trueArray += ", True";
               emptyLabels += ", ";
            }
            Modality m = aligned.get( i );
            emptyLabels += "'" + m.getName() + "'";

            alignedModalityPipelineSources[i] = db.createPipelineSource( m.getSource(), m.getModalityId(), -1 );
            // alignments are probably used for annotations, it makes more sense to use the name of the modality,
            // instead of the name of the source
            alignedModalityPipelineSources[i].name = m.getName();
         }
         trueArray += "]";
         emptyLabels += "]";
         // add the visibility/label parameters
         try {
            // this parameter should match the one in the python server script
            String parameterName = "alignedModalityVisibilities";
            Parameter parameter = Parameter.newInstance( -1, parameterName, Parameter.ParameterType.BOOLEAN.name(),
                  false, aligned.size(), trueArray, true, true, parameterName, parameterName,
                  Parameter.SetterType.ASSIGNMENT.name(), false );
            alignedModalityParameters.put( parameter.parameterName, parameter );

            // register the parameter with the database (or get a parameter id)
            db.registerCustomSessionParameter( parameter );

            // this parameter should match the one in the python server script
            parameterName = "alignedModalityLabels";

            parameter = Parameter.newInstance( -1, parameterName, Parameter.ParameterType.STRING.name(), false,
                  aligned.size(), emptyLabels, true, false, parameterName, null, Parameter.SetterType.NONE.name(),
                  false );
            alignedModalityParameters.put( parameter.parameterName, parameter );

            // register the parameter with the database (or get a parameter id)
            db.registerCustomSessionParameter( parameter );
         }
         catch( ParameterException pe ) {
            log.error( "Could not add alignedModalityLabels/alignedModalityVisibilities parameters for modality " + modality.getName() );
         }

      } else {
         alignedModalityPipelineSources = null;
      }

      // the visualization pipeline
      pipeline = db.createPipeline( visualization, modality.getModalityId() );
      log.info( "Pipeline (" + pipeline.name + ") initialized" );

      // the outputs
      pipelineOutputs = db.createPipelineOutputs( visualization.getOutputs().keySet(), modality.getModalityId(),
            visualization.visualizationId );

      log.info( "PipelineOutputs (count: " + pipelineOutputs.size() + ") initialized" );

      // start the session idle check
      new Thread( this ).start();
   }

   public void run() {
      boolean running = true;
      while( running ) {
         if( lastAction > 0 && ( System.currentTimeMillis() - lastAction ) > Application.config.sessionTimeout && status == Status.IDLE ) {
            // close the session
            reason = "Session timed out (idle timeout of " + ( Application.config.sessionTimeout / 1000 ) + " seconds)";
            closeSession( true );
            running = false;
         } else {
            try {
               Thread.sleep( 500 );
            }
            catch( InterruptedException ie ) {
               // ignore
            }
         }
      }
      log.info( "Session was closed because of a timeout" );
      // the session object will be kept until the Application.config.closedSessionRemoveTimeout expires
   }

   private void addDefaultSessionParameters() {
      addDefaultSessionParameters( true, "StoreLocation", ParameterType.STRING, false, 1, null, false, false,
            "StoreLocation", "StoreLocation", SetterType.ASSIGNMENT );
   }

   private void addDefaultSessionParameters( boolean isSessionParameter, String parameterName, ParameterType type,
         boolean isTuple, int parameterLength, String pythonValue, boolean isUserViewable, boolean isUserModifiable,
         String parameterGetter, String parameterSetter, SetterType setterType ) {
      try {
         if( isSessionParameter ) {
            Parameter parameter = Parameter.newInstance( 0, parameterName, type.name(), isTuple, parameterLength,
                  pythonValue, isUserViewable, isUserModifiable, parameterGetter, parameterSetter, setterType.name(),
                  false );

            parameter.setSessionParameter( true );
            sessionParameters.put( parameter.parameterName, parameter );
         }
      }
      catch( ParameterException pe ) {
         log.error( "Could not create session parameter " + parameterName, pe );
         // ignore anyway
      }
   }

   /**
    * The worker thread will call this method to signal that properties/statuses of the job may have changed
    * 
    * @param j
    */
   public void jobAltered( Job j ) {
      try {
         readLock.lock();

         synchronized( this ) {
            if( alteredJobs.contains( j ) == false )
               alteredJobs.add( j );

            // re-evaluate the session status

            // first check if the job that reported its change is the last
            // running or waiting job in this session
            if( j == currentJob && j.getStatus() == Job.Status.FAILED || j.getStatus() == Job.Status.FINISHED ) {
               currentJob = null;
            }

            // don't reset the status when the session was already closed
            if( status == Status.CLOSED || status == Status.FAILED ) {
               setSessionAltered( true );
               return;
            }

            for( Job waitingJob : waitingJobs ) {
               Job.Status waitingJobStatus = waitingJob.getStatus();
               if( waitingJobStatus == Job.Status.WORKING || waitingJobStatus == Job.Status.WAITING ) {
                  status = Status.WORKING;
                  setSessionAltered( true );
                  return;
               }
            }

            // one option for this session to be still working:
            if( currentJob != null && ( currentJob.status == Job.Status.WAITING || currentJob.status == Job.Status.WORKING ) ) {
               status = Status.WORKING;
            } else {
               algorithm = "";
               progress = 0;
               status = Status.IDLE;
            }

            setSessionAltered( true );
         }
      }
      finally {
         readLock.unlock();
      }
   }

   public void clearAlteredJobs() {
      try {
         readLock.lock();

         synchronized( this ) {
            alteredJobs.clear();
         }
      }
      finally {
         readLock.unlock();
      }

   }

   public void addJob( Job j ) {
      try {
         readLock.lock();

         synchronized( this ) {
            if( status == Status.CLOSED || status == Status.FAILED )
               return;

            waitingJobs.add( j );
            setSessionAltered( true );
         }
      }
      finally {
         readLock.unlock();
      }
   }

   public void closeSession( boolean removeJobs ) {
      try {
         readLock.lock();
         synchronized( this ) {
            if( removeJobs )
               waitingJobs.clear();

            setStatus( Status.CLOSED );
         }
      }
      finally {
         readLock.unlock();
      }

   }

   /**
    * Retrieve the amount of jobs waiting to be picked up in this session. We assume that the only calling class will be
    * the worker appointed to the session (otherwise we should synchronize this method)
    * 
    * @return
    */
   public int getJobsWaiting() {
      return waitingJobs.size();
   }

   public Modality getModality() {
      return modality;
   }

   /**
    * Request the next waiting job from the session and set this job to WORKING status.
    * 
    * @return
    */

   public Job getNextJob() {
      if( waitingJobs.size() == 0 )
         return null;

      try {
         readLock.lock();
         synchronized( this ) {
            currentJob = waitingJobs.remove( 0 );
            currentJob.setStatus( Job.Status.WORKING );
            log.debug( "Job status to working: " + currentJob );
         }
      }
      finally {
         readLock.unlock();
      }
      return currentJob;
   }

   /**
    * This method will fetch a parameter from the session. The parameter can come from one of the pipeline components
    * (the source-, pipeline-, annotation- or session parameters). When null is specified for the output, the
    * selectedOutput (if set) is checked for the parameter, otherwise, the specified output is checked for the
    * parameter.
    * 
    * Note. This method is not synchronized because we assume that only object calling this method at any time will be
    * the worker for this session.
    * 
    * @param parameterName
    * @param output
    * @return
    */

   public Parameter getParameter( String parameterName, String output ) {
      Parameter p;
      // check session parameters
      p = sessionParameters.get( parameterName );
      if( p != null )
         return p;

      // check annotation parameters
      p = alignedModalityParameters.get( parameterName );
      if( p != null )
         return p;

      // check source
      if( pipelineSource != null ) {
         p = pipelineSource.getParameter( parameterName );
         if( p != null )
            return p;
      }
      // check pipeline
      if( pipeline != null ) {
         p = pipeline.getParameter( parameterName );
         if( p != null )
            return p;
      }
      // otherwise check selected output
      if( output != null ) {
         PipelineOutput o = pipelineOutputs.get( output );
         if( o != null ) {
            p = o.getParameter( parameterName );
            if( p != null )
               return p;
         }
      } else {
         if( selectedOutput != null ) {
            p = selectedOutput.getParameter( parameterName );
            if( p != null )
               return p;
         }
      }
      return null;
   }

   public Pipeline getPipeline() {
      return pipeline;
   }

   public PipelineSource getPipelineSource() {
      return pipelineSource;
   }

   public String getProgress() {
      return "[" + algorithm + ": " + progress + "%]";
   }

   public PipelineOutput getSelectedOutput() {
      return selectedOutput;
   }

   public Status getStatus() {
      return status;
   }

   public Visualization getVisualization() {
      return visualization;
   }

   public boolean checkIfOutputExists( String output ) {
      return( pipelineOutputs != null && pipelineOutputs.containsKey( output ) );
   }

   /**
    * This method should reset all caches of parameters lists that depend on some output to be selected. When a new
    * output is set (usually in every job), these caches should be cleared.
    * 
    */
   private void clearOuputDependentCaches() {
      allParameters = null;
      allVisualizationParameters = null;
   }

   public void setOutput( String outputName ) throws UserRequestException {
      // don't reset the output when the correct output is already selected (to prevent the cache to be reset below)
      if( selectedOutput != null && selectedOutput.name.equalsIgnoreCase( outputName ) )
         return;

      PipelineOutput output = pipelineOutputs.get( outputName );
      if( output == null )
         throw new UserRequestException( "Output '" + outputName + "' does not exist" );

      selectedOutput = output;

      // reset the cached parameter list
      clearOuputDependentCaches();
   }

   public void setProgress( String algorithm, int progress ) {
      try {
         readLock.lock();
         synchronized( this ) {
            if( this.algorithm.equalsIgnoreCase( algorithm ) == false && this.progress != progress ) {
               this.algorithm = algorithm;
               this.progress = progress;
               setSessionAltered( true );
            }
         }
      }
      finally {
         readLock.unlock();
      }

   }

   public void setStatus( Session.Status newStatus ) {
      try {
         readLock.lock();
         synchronized( this ) {
            if( status != Status.CLOSED && status != Status.FAILED && status != newStatus ) {
               this.status = newStatus;
               if( newStatus == Status.CLOSED || newStatus == Status.FAILED ) {
                  startRemoveTimer();
               }
               setSessionAltered( true );
            }
         }
      }
      finally {
         readLock.unlock();
      }

   }

   public void setStatusFailed( String reason ) {
      try {
         readLock.lock();
         synchronized( this ) {
            this.reason = reason;
            this.status = Status.FAILED;
            this.setProgress( reason, -1 );
            startRemoveTimer();
         }
      }
      finally {
         readLock.unlock();
      }

   }

   /**
    * This method returns true when the session is not closed, and not failed
    * 
    * @return True when the session is not closed or failed
    */

   public boolean isSessionActive() {
      try {
         readLock.lock();

         synchronized( this ) {
            return status != Status.FAILED && status != Status.CLOSED;
         }
      }
      finally {
         readLock.unlock();
      }

   }

   /**
    * This method starts a thread which will remove the session from the sessionManager after a timeout of
    * 'closedSessionRemoveTimeout' milliseconds
    */

   private void startRemoveTimer() {
      final Session thisSession = this; // we cannot refer to 'this' object below, because that will refer to the
      // inner class itself
      new Thread( new Runnable() {
         public void run() {
            try {
               Thread.sleep( Application.config.closedSessionRemoveTimeout );
            }
            catch( InterruptedException ie ) {
               // ignore
            }
            log.info( "killTimer expired, removing session " + thisSession );
            SessionManager.getInstance().removeSession( thisSession );
         }
      } ).start();
   }

   /**
    * Return all parameters that can be used in this session, and collect them in reversed pipeline order (so: reversed
    * to the order of finding parameters in getParameter call above)
    * 
    */
   public HashMap<String, Parameter> getAllParameters() {
      if( allParameters == null ) {
         allParameters = new HashMap<String, Parameter>();

         if( selectedOutput != null ) {
            allParameters.putAll( selectedOutput.getParameters() );
         }

         allParameters.putAll( pipeline.getParameters() );
         allParameters.putAll( pipelineSource.getParameters() );
         allParameters.putAll( alignedModalityParameters );
         allParameters.putAll( sessionParameters );
      }

      return allParameters;
   }

   /**
    * Return all parameters that can be used in this session, and collect them in reversed pipeline order (so: reversed
    * to the order of finding parameters in getParameter call). This call is used to find all parameters that could be
    * used to create the visualization.
    * 
    * (this method is called from the dataManager/database code to store/retrieve a lookmark)
    * 
    */
   public HashMap<String, Parameter> getAllVisualizationParameters() {
      if( allVisualizationParameters == null ) {
         // get all the parameters
         allVisualizationParameters = new HashMap<String, Parameter>( getAllParameters() );
         // now remove the session parameters
         for( Parameter p : sessionParameters.values() ) {
            allVisualizationParameters.remove( p.parameterName );
         }
      }
      return allVisualizationParameters;
   }

   /**
    * This method returns all parameters in the session for a given output (this method is called indirectly from the
    * Webservice, to provide a user with its parameters)
    * 
    */
   public HashMap<String, Parameter> getAllVisualizationParametersForOutput( String outputName ) {
      HashMap<String, Parameter> parameters = new HashMap<String, Parameter>();

      if( outputName != null ) {
         HashMap<String, Parameter> outParams = getOutputParameters( outputName );
         if( outParams != null )
            parameters.putAll( outParams );

      }
      parameters.putAll( pipeline.getParameters() );
      parameters.putAll( pipelineSource.getParameters() );
      parameters.putAll( alignedModalityParameters );
      return parameters;
   }

   public boolean isOutputValid( String outputName ) {
      if( outputName == null || outputName.length() == 0 )
         return true; // just no output specified, that's possible
      // an output is specified, this should exist, otherwise return false
      return pipelineOutputs.get( outputName ) != null;
   }

   public HashMap<String, Parameter> getOutputParameters( String outputName ) {
      PipelineOutput output = pipelineOutputs.get( outputName );
      if( output == null )
         return null;
      return output.getParameters();
   }

   public String[] getAllGettableParameters() {
      HashMap<String, Parameter> params = getAllParameters();

      ArrayList<Parameter> gettables = new ArrayList<Parameter>();
      // now remove all parameters without a GETTER
      for( Parameter p : params.values() ) {
         if( p.hasGetter() ) {
            gettables.add( p );
         }
      }

      String[] paramNames = new String[ gettables.size() ];
      int i = 0;
      for( Parameter p : gettables ) {
         paramNames[i++] = p.parameterName;
      }

      return paramNames;
   }

   public void incrementRetryCount() {
      this.retryCount++;
   }

   public int getRetryCount() {
      return retryCount;
   }

   /**
    * This method will return the status of the 'sessionAltered' boolean and makes sure that it gets reset to false
    * afterwards.
    * 
    * @return
    */
   public boolean getAndResetSessionAltered() {
      try {
         readLock.lock();

         synchronized( this ) {
            if( !sessionAltered )
               return false;

            sessionAltered = false;

            return true;
         }
      }
      finally {
         readLock.unlock();
      }

   }

   /**
    * This can set the boolean specifiying whether the session has been altered. This method will also reset the
    * lastAction timestamp, to prevent closing the session after the timeout.
    * 
    * @param sessionAltered
    */
   public void setSessionAltered( boolean sessionAltered ) {
      try {
         readLock.lock();
         synchronized( this ) {
            this.sessionAltered = sessionAltered;
            if( this.status == Status.IDLE ) {
               this.lastAction = System.currentTimeMillis();
            } else {
               this.lastAction = -1;
            }
         }
      }
      finally {
         readLock.unlock();
      }

   }

   public PipelineSource[] getAlignedModalityPipelineSources() {
      return alignedModalityPipelineSources;
   }

   public String getFailureReason() {
      return reason;
   }

   public HashMap<String, Parameter> getSessionParameters() {
      return sessionParameters;
   }

   /**
    * This method will return all parameters that were added on-the-fly within the session, to support aligned
    * modalities within the pipeline scripts
    * 
    * @return
    */

   public HashMap<String, Parameter> getAlignedModalityParameters() {
      HashMap<String, Parameter> alignedModalityParameters = new HashMap<String, Parameter>(
            this.alignedModalityParameters );
      return alignedModalityParameters;
   }

   /**
    * This method will return all parameters that were added on-the-fly within the session, to support aligned
    * modalities within the pipeline scripts. Only the parameters with a GETTER will be returned (used while
    * initializing the visualization pipeline)
    * 
    * @return
    */

   public HashMap<String, Parameter> getAlignedModalityParametersWithGetters() {
      HashMap<String, Parameter> alignedModalityParameters = new HashMap<String, Parameter>();
      for( Parameter p : this.alignedModalityParameters.values() ) {
         if( p.hasGetter() )
            alignedModalityParameters.put( p.parameterName, p );
      }
      return alignedModalityParameters;
   }
}
